using System;
using System.Collections.Generic;

namespace Fadd.Commands
{
    /// <summary>
    /// <para>
    /// This dispatcher is used to be able to specify which commands that can be invoked.
    /// </para>
    /// </summary>
    /// <remarks>
    /// <para>
    /// A typical usage would be if you have a plugin system and you do not want to allow
    /// all plugins to be able to execute all commands. Then you create a proxy dispatcher
    /// and expose it to the plugins, instead of the core dispatcher which contains all command 
    /// mappings. In this way, your core system it still protected while plugins can access
    /// a subset of it.
    /// </para>
    /// </remarks>
    public class FilteredDispatcher : ICommandDispatcher
    {
        private static readonly List<Type> EmptyList = new List<Type>();
        private readonly ICommandDispatcher _dispatcher;
        private readonly List<Type> _executeCommands;
        private readonly List<Type> _handledCommands;
        private readonly ICommandDispatcher _parent;
        /// <summary>
        /// Occurs when an unhandled exception have been caught.
        /// </summary>
        public event UnhandledExceptionDelegate UnhandledExceptionThrown;

        /// <summary>
        /// This event is invoked when someone tries to add a handler for a command.
        /// </summary>
        public event FilterHandler AddRequested;

        /// <summary>
        /// This event is invoked when someone tries to invoke a command.
        /// </summary>
        public event FilterHandler InvokeRequested;

        /// <summary>
        /// Initializes a new instance of the <see cref="FilteredDispatcher"/> class.
        /// </summary>
        /// <param name="parent">Parent dispatcher (that is used by the rest of the system).</param>
        /// <param name="executeCommands">Commands that may be invoked.</param>
        /// <param name="handledCommands">Commands that may be handled.</param>
        public FilteredDispatcher(ICommandDispatcher parent, List<Type> executeCommands, List<Type> handledCommands)
        {
            if (parent == null)
                throw new ArgumentNullException("parent");
            if (executeCommands == null)
                throw new ArgumentNullException("executeCommands");
            if (handledCommands == null)
                throw new ArgumentNullException("handledCommands");

            _parent = parent;
            _dispatcher = new CommandManager(null, false);
            _executeCommands = executeCommands;
            _handledCommands = handledCommands;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="FilteredDispatcher"/> class.
        /// </summary>
        /// <param name="parent">Parent dispatcher (that is used by the rest of the system).</param>
        /// <remarks>No commands are allowed unless you use the <see cref="InvokeRequested"/> event.</remarks>
        public FilteredDispatcher(ICommandDispatcher parent)
            : this(parent, EmptyList, EmptyList)
        {
        }

        #region ICommandDispatcher Members

        /// <summary>
        /// Add a command handler.
        /// </summary>
        /// <param name="type">Must be a class, an attribute.</param>
        /// <param name="handler">handler handling the command</param>
        /// <exception cref="ArgumentException">If handler have been added to that type already.</exception>
        /// <exception cref="InvalidOperationException">The specified command may not be handled.</exception>
        /// <example>
        /// Program.Commands.Add(typeof(MyCommand), OnMyCommand);
        /// </example>
        public void Add(Type type, CommandHandler handler)
        {
            if (!typeof (Command).IsAssignableFrom(type))
                throw new ArgumentException("Only commands may be added in this implementation.");
            if (AddRequested != null)
                AddRequested(this, new FilterEventArgs(type));
            else if (!_handledCommands.Contains(type))
                throw new InvalidOperationException("You may not handle that command.");

            bool exists = _dispatcher.Contains(type);

            // Add the handler
            _dispatcher.Add(type, handler);

            // Add it to our real system wide dispatcher if it has not been added.
            if (!exists)
                _parent.Add(type, OnCommand);
        }

        /// <summary>
        /// Remove a command handler.
        /// </summary>
        /// <param name="type">type to remove</param>
        /// <param name="handler">delegated that was mapped to the type.</param>
        public void Remove(Type type, CommandHandler handler)
        {
            _dispatcher.Remove(type, handler);

            // Unhook this proxy if no handlers are left.
            if (!_dispatcher.Contains(type))
                _parent.Remove(type, OnCommand);
        }

        /// <summary>
        /// Invoke a command.
        /// </summary>
        /// <param name="source">object that is invoking the command.</param>
        /// <param name="command">command to invoke.</param>
        /// <param name="ignoreMe">Handled that should not be invoked.</param>
        /// <returns>true if command was handled.</returns>
        public bool Invoke(object source, Command command, CommandHandler ignoreMe)
        {
            Type type = command.GetType();
            if (InvokeRequested != null)
                InvokeRequested(this, new FilterEventArgs(type));
            else if (!_executeCommands.Contains(type))
                throw new InvalidOperationException("Unauthorized attempt to invoke " + command.GetType().FullName + ".");

            // we will be calling ourself too since we've hooked up all requested commands
            return _parent.Invoke(source ?? this, command, ignoreMe);
        }

        /// <summary>
        /// Invoke a command.
        /// </summary>
        /// <param name="command">command to invoke.</param>
        /// <returns>true if command was handled.</returns>
        /// <exception cref="InvalidOperationException">If you may not invoke the specified command.</exception>
        public bool Invoke(Command command)
        {
            return Invoke(command, null);
        }

        /// <summary>
        /// Invoke a command.
        /// </summary>
        /// <param name="command">command to invoke.</param>
        /// <param name="ignoreMe">Handled that should not be invoked.</param>
        /// <returns>true if command was handled.</returns>
        public bool Invoke(Command command, CommandHandler ignoreMe)
        {
            return Invoke(null, command, ignoreMe);
        }

        /// <summary>
        /// Invoke a command.
        /// </summary>
        /// <param name="source">object that is invoking the command.</param>
        /// <param name="command">command to invoke.</param>
        /// <returns>true if command was handled.</returns>
        public bool Invoke(object source, Command command)
        {
            return Invoke(source, command, null);
        }

        /// <summary>
        /// Invoke a command asynchronously
        /// </summary>
        /// <param name="source">object that is invoking the command.</param>
        /// <param name="command">Command to invoke</param>
        /// <param name="callback">Callback that is invoked then the command completes.</param>
        /// <param name="state">object that you can use to identify the command in the <see cref="AsyncCallback"/>-method.</param>
        /// <returns>IAsyncResult if command was invoked successfully; otherwise null.</returns>
        /// <exception cref="InvalidOperationException">If you may not invoke the specified command.</exception>
        public IAsyncResult BeginInvoke(object source, Command command, AsyncCallback callback, object state)
        {
            return BeginInvoke(source, command, null, callback, state);
        }

        /// <summary>
        /// Invoke a command asynchronously
        /// </summary>
        /// <param name="command">Command to invoke</param>
        /// <param name="source">object that is invoking the command.</param>
        /// <param name="ignoreMe">Handler that should not receive the command.</param>
        /// <param name="callback">Callback that is invoked then the command completes.</param>
        /// <param name="state">object that you can use to identify the command in the <see cref="AsyncCallback"/>-method.</param>
        /// <returns>IAsyncResult if command was invoked successfully; otherwise null.</returns>
        public IAsyncResult BeginInvoke(object source, Command command, CommandHandler ignoreMe, AsyncCallback callback, object state)
        {
            Type type = command.GetType();
            if (InvokeRequested != null)
                InvokeRequested(this, new FilterEventArgs(type));
            else if (!_executeCommands.Contains(type))
                throw new InvalidOperationException("Unauthorized attempt to invoke " + command.GetType().FullName + ".");

            return _parent.BeginInvoke(source, command, ignoreMe, callback, state);
        }

        /// <summary>
        /// Invoke a command asynchronously
        /// </summary>
        /// <param name="command">Command to invoke</param>
        /// <returns>IAsyncResult if command was invoked successfully; otherwise null.</returns>
        /// <param name="callback">Callback that is invoked then the command completes.</param>
        /// <param name="state">object that you can use to identify the command in the <see cref="AsyncCallback"/>-method.</param>
        public IAsyncResult BeginInvoke(Command command, AsyncCallback callback, object state)
        {
            return BeginInvoke(null, command, null, callback, state);
        }

        /// <summary>
        /// Invoke a command asynchronously
        /// </summary>
        /// <param name="command">Command to invoke</param>
        /// <returns>IAsyncResult if command was invoked successfully; otherwise null.</returns>
        /// <param name="ignoreMe">Handler that should not receive the command.</param>
        /// <param name="callback">Callback that is invoked then the command completes.</param>
        /// <param name="state">object that you can use to identify the command in the <see cref="AsyncCallback"/>-method.</param>
        public IAsyncResult BeginInvoke(Command command, CommandHandler ignoreMe, AsyncCallback callback, object state)
        {
            return BeginInvoke(null, command, ignoreMe, callback, state);
        }

        /// <summary>
        /// Invoke this method when the command is complete, or if you want to wait
        /// on the command.
        /// </summary>
        /// <param name="res"></param>
        /// <returns></returns>
        public Command EndInvoke(IAsyncResult res)
        {
            return _parent.EndInvoke(res);
        }

        /// <summary>
        /// Tells us if we have a handler for the specified type.
        /// </summary>
        /// <param name="type">Type to check</param>
        /// <returns>True if a handler have been registered otherwise false.</returns>
        public bool Contains(Type type)
        {
            return _dispatcher.Contains(type) || _parent.Contains(type);
        }

        /// <summary>
        /// Handler for unhandled commands.
        /// </summary>
        /// <remarks>returning true will make the command look like it was handled</remarks>
        public event CommandHandler Unhandled
        {
            add { _dispatcher.Unhandled += value; }
            remove { _dispatcher.Unhandled -= value; }
        }

        #endregion

        private bool OnCommand(object source, CommandEventArgs args)
        {
            try
            {
                return _dispatcher.Invoke(source, args.Command);    
            }
            catch (Exception err)
            {
                if (UnhandledExceptionThrown == null)
                    throw;

                UnhandledExceptionThrown(this, new UnhandledExceptionEventArgs(err));
                return false;
            }
        }
    }
}